<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #container {
      width: 100%;
      margin: 50px auto 0;
    }
  </style>
</head>

<body>
  <div id="container">
    <button onclick="zoomOut()">放大</button>
    <button onclick="zoomIn()">缩小</button>
    <button onclick="zoomCenter()">居中</button>
  </div>
</body>

</html>
<script src="https://d3js.org/d3.v5.min.js"></script>

<script>

  let transform = {
    x: 0,
    y: 0,
    k: 1,
  };

  let zoom = null;
  const zoomMin = 0.3;
  const zoomMax = 5;
  const width = 20000;
  const height = 20000;

  const svg = d3.select('#container')
    .append('svg')
    .attr('width', width)
    .attr('height', width);

  const container = svg.append('g')
    .attr(
      'transform',
      `translate(${transform.x},${transform.y} ) scale(${transform.k})`,
    );

  const data = [
    { id: 1, fill: 'black', x: 10, y: 10 },
    { id: 2, fill: 'black', x: 50, y: 50 },
    { id: 3, fill: 'black', x: 100, y: 70 },
    { id: 4, fill: 'black', x: 20, y: 100 }
  ];

  draw(); // 绘制
  bindZoom(); //绑定zoom
  function draw() {
    const update = container.selectAll('rect')
      .data(data, d => d.id);

    //修改层
    update.attr('x', (d, idx) => d.x)
      .attr('y', (d, idx) => d.y)
      .attr('fill', (d) => d.fill)

    //渲染层
    const enter = update.enter();

    //删除层
    const exit = update.exit();

    enter.append('rect')
      .attr('width', 20)
      .attr('height', 20)
      .attr('id', d => d.id)
      .attr('x', (d, idx) => d.x)
      .attr('y', (d, idx) => d.y)
      .attr('fill', (d) => d.fill)
      .attr('stroke', 'blue')
      .attr('strokeWidth', 1)

    exit.remove()
  }

  function bindZoom() {
    zoom = d3.zoom()
      .scaleExtent([zoomMin, zoomMax])
      .on('zoom', function () {
        transform = d3.zoomTransform(this);
        container.attr('transform', transform);
        console.log(transform, 'transform...')
      })

    svg.call(zoom).on('dblclick.zoom', null);
  };

  function stopZoom() { //解除zoom绑定
    svg.on('mousedown.zoom', null);
    svg.on('mousemove.zoom', null);
    svg.on('dblclick.zoom', null);
    svg.on('touchstart.zoom', null);
  };


  function zoomIn() {
    svg.transition().call(zoom.scaleBy, 0.7);
  };

  function zoomOut() {
    svg.transition().call(zoom.scaleBy, 1.3);
  };

  function zoomCenter() {
    const containerX = data.map((item) => item.x);
    const containerY = data.map((item) => item.y);

    const minX = Math.min.apply(null, containerX);
    const maxX = Math.max.apply(null, containerX);
    const minY = Math.min.apply(null, containerY);
    const maxY = Math.max.apply(null, containerY);

    const containerGroupBBox = { // 用数据获取到当前group得w、h、x、y
      width: maxX - minX,
      height: maxY - minY,
      x: minX,
      y: minY,
    };

    const scaleX = width / containerGroupBBox.width;
    const scaleY = height / containerGroupBBox.height;

    let k = Math.min(scaleX, scaleY) * 0.7;
    k = Math.max(k, zoomMin);
    k = Math.min(k, zoomMax); // 用画布大小 / 当前svg得大小 获取到比例值

    // 算出居中得x、y坐标（往俩盒子 一个大盒子（画布）和另外一个盒子（图形撑起来得））怎么让图形撑起来得居中呢！！！
    // 用画布自身得一半 减去 gropu得一半 * 缩放 K, 再减去gropu得translate 
    const translateByX = width / 2 - (containerGroupBBox.width / 2) * k - (containerGroupBBox.x * k);
    const translateByY = height / 2 - (containerGroupBBox.height / 2) * k - (containerGroupBBox.y * k);


    const transform = d3.zoomIdentity
      .translate(translateByX, translateByY)
      .scale(k); // 获取到目标 transform

    svg.transition() // 过渡效果
      .duration(100) // 100ms
      .call(zoom.transform, transform);
  }
</script>
